using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Reflection;

namespace Comms {
    public class Peer : IDisposable {
        public abstract class Message {
            public static Dictionary<int, Type> MessageTypesByMessageId;

            public static Dictionary<Type, int> MessageIdsByMessageTypes;

            static Message() {
                MessageTypesByMessageId = new Dictionary<int, Type>();
                MessageIdsByMessageTypes = new Dictionary<Type, int>();
                TypeInfo[] array = (from t in typeof(Message).GetTypeInfo().Assembly.DefinedTypes
                    where typeof(Message).GetTypeInfo().IsAssignableFrom(t)
                    orderby t.Name
                    select t).ToArray();
                for (int i = 0; i < array.Length; i++) {
                    MessageTypesByMessageId[i] = array[i].AsType();
                    MessageIdsByMessageTypes[array[i].AsType()] = i;
                }
            }

            public static Message Read(byte[] bytes) {
                Reader reader = new(bytes);
                byte key = reader.ReadByte();
                if (MessageTypesByMessageId.TryGetValue(key, out Type value)) {
                    Message obj = (Message)Activator.CreateInstance(value);
                    obj?.Read(reader);
                    return obj;
                }
                return null;
            }

            public static byte[] Write(Message message) {
                Writer writer = new();
                writer.WriteByte((byte)MessageIdsByMessageTypes[message.GetType()]);
                message.Write(writer);
                return writer.GetBytes();
            }

            public static void Handle(Peer peer, Packet packet) {
                Message message = Read(packet.Bytes);
                if (message != null) {
                    message.Handle(peer, packet.Address);
                    return;
                }
                throw new ProtocolViolationException("Unrecognized message, ignoring.");
            }

            public abstract void Read(Reader reader);

            public abstract void Write(Writer writer);

            public virtual void Handle(Peer peer, IPEndPoint address) { }
        }

        public class DiscoveryRequestMessage : Message {
            public byte[] DiscoveryRequestData;

            public override void Read(Reader reader) {
                DiscoveryRequestData = reader.ReadFixedBytes(reader.Length - reader.Position);
            }

            public override void Write(Writer writer) {
                writer.WriteFixedBytes(DiscoveryRequestData);
            }

            public override void Handle(Peer peer, IPEndPoint address) {
                if (peer.PeerDiscoveryRequest != null) {
                    peer.PeerDiscoveryRequest(new Packet(address, DiscoveryRequestData));
                }
                else {
                    peer.RespondToDiscovery(address, DeliveryMode.Raw);
                }
            }
        }

        public class DiscoveryResponseMessage : Message {
            public byte[] DiscoveryResponseData;

            public override void Read(Reader reader) {
                DiscoveryResponseData = reader.ReadFixedBytes(reader.Length - reader.Position);
            }

            public override void Write(Writer writer) {
                writer.WriteFixedBytes(DiscoveryResponseData);
            }

            public override void Handle(Peer peer, IPEndPoint address) {
                peer.PeerDiscovered?.Invoke(new Packet(address, DiscoveryResponseData));
            }
        }

        public class ConnectRequestMessage : Message {
            public byte[] ConnectRequestData;

            public override void Read(Reader reader) {
                ConnectRequestData = reader.ReadFixedBytes(reader.Length - reader.Position);
            }

            public override void Write(Writer writer) {
                writer.WriteFixedBytes(ConnectRequestData);
            }

            public override void Handle(Peer peer, IPEndPoint address) {
                if (peer.FindPeer(address) == null) {
                    PeerData peerData = new(peer, address);
                    if (peer.ConnectRequest != null) {
                        peer.ConnectRequest(new PeerPacket(peerData, ConnectRequestData));
                    }
                    else {
                        peer.AcceptConnect(peerData);
                    }
                    return;
                }
                throw new ProtocolViolationException("Connect request ignored, peer already connected.");
            }
        }

        public class ConnectAcceptedMessage : Message {
            public IPEndPoint[] PeersAddresses;

            public byte[] ConnectAcceptedData;

            public override void Read(Reader reader) {
                int num = reader.ReadUInt16();
                PeersAddresses = new IPEndPoint[num];
                for (int i = 0; i < num; i++) {
                    PeersAddresses[i] = reader.ReadIPEndPoint();
                }
                ConnectAcceptedData = reader.ReadFixedBytes(reader.Length - reader.Position);
            }

            public override void Write(Writer writer) {
                int num = Math.Min(PeersAddresses.Length, 65535);
                writer.WriteUInt16((ushort)num);
                for (int i = 0; i < num; i++) {
                    writer.WriteIPEndPoint(PeersAddresses[i]);
                }
                writer.WriteFixedBytes(ConnectAcceptedData);
            }

            public override void Handle(Peer peer, IPEndPoint address) {
                if (peer.ConnectedTo != null) {
                    throw new ProtocolViolationException("Unexpected connection accept ignored, peer already connected.");
                }
                if (!Equals(peer.ConnectingTo, address)) {
                    throw new ProtocolViolationException($"Unexpected connection accept from {address} ignored.");
                }
                peer.ConnectedTo = new PeerData(peer, address);
                peer.ConnectingTo = null;
                peer.PeersByAddress.Clear();
                IPEndPoint[] peersAddresses = PeersAddresses;
                foreach (IPEndPoint iPEndPoint in peersAddresses) {
                    peer.PeersByAddress.Add(iPEndPoint, new PeerData(peer, iPEndPoint));
                }
                peer.ConnectAccepted?.Invoke(new PeerPacket(peer.ConnectedTo, ConnectAcceptedData));
            }
        }

        public class ConnectRefusedMessage : Message {
            public byte[] ConnectRefusedData;

            public override void Read(Reader reader) {
                ConnectRefusedData = reader.ReadFixedBytes(reader.Length - reader.Position);
            }

            public override void Write(Writer writer) {
                writer.WriteFixedBytes(ConnectRefusedData);
            }

            public override void Handle(Peer peer, IPEndPoint address) {
                if (peer.ConnectedTo != null
                    || !Equals(peer.ConnectingTo, address)) {
                    throw new ProtocolViolationException("Unexpected connection refuse ignored.");
                }
                peer.ConnectingTo = null;
                peer.ConnectRefused?.Invoke(new Packet(address, ConnectRefusedData));
            }
        }

        public class DisconnectRequestMessage : Message {
            public override void Read(Reader reader) { }

            public override void Write(Writer writer) { }

            public override void Handle(Peer peer, IPEndPoint address) {
                PeerData peerData = peer.FindPeer(address);
                if (peerData == null) {
                    return;
                }
                peer.PeersByAddress.Remove(address);
                if (peer.Settings.SendPeerConnectDisconnectNotifications) {
                    foreach (PeerData value in peer.PeersByAddress.Values) {
                        peer.publicSend(value.Address, DeliveryMode.ReliableSequenced, new PeerDisconnectedMessage { PeerAddress = address });
                    }
                }
                peer.PeerDisconnected?.Invoke(peerData);
            }
        }

        public class DisconnectedMessage : Message {
            public override void Read(Reader reader) { }

            public override void Write(Writer writer) { }

            public override void Handle(Peer peer, IPEndPoint address) {
                if (peer.ConnectedTo != null) {
                    peer.publicDisconnect();
                }
            }
        }

        public class PeerConnectedMessage : Message {
            public IPEndPoint PeerAddress;

            public override void Read(Reader reader) {
                PeerAddress = reader.ReadIPEndPoint();
            }

            public override void Write(Writer writer) {
                writer.WriteIPEndPoint(PeerAddress);
            }

            public override void Handle(Peer peer, IPEndPoint address) {
                if (peer.ConnectedTo != null) {
                    if (peer.FindPeer(address) != null) {
                        throw new ProtocolViolationException("Peer connection notification ignored, peer already connected.");
                    }
                    PeerData peerData = new(peer, PeerAddress);
                    peer.PeersByAddress.Add(PeerAddress, peerData);
                    peer.PeerConnected?.Invoke(peerData);
                }
            }
        }

        public class PeerDisconnectedMessage : Message {
            public IPEndPoint PeerAddress;

            public override void Read(Reader reader) {
                PeerAddress = reader.ReadIPEndPoint();
            }

            public override void Write(Writer writer) {
                writer.WriteIPEndPoint(PeerAddress);
            }

            public override void Handle(Peer peer, IPEndPoint address) {
                if (peer.ConnectedTo != null) {
                    PeerData peerData = peer.FindPeer(PeerAddress);
                    if (peerData != null) {
                        peer.PeersByAddress.Remove(PeerAddress);
                        peer.PeerDisconnected?.Invoke(peerData);
                    }
                }
            }
        }

        public class KeepAliveRequestMessage : Message {
            public double RequestSendTime;

            public override void Read(Reader reader) {
                RequestSendTime = reader.ReadDouble();
            }

            public override void Write(Writer writer) {
                writer.WriteDouble(RequestSendTime);
            }

            public override void Handle(Peer peer, IPEndPoint address) {
                PeerData peerData = peer.ConnectedTo != null && Equals(address, peer.ConnectedTo.Address) ? peer.ConnectedTo : peer.FindPeer(address);
                if (peerData != null) {
                    peerData.LastKeepAliveReceiveTime = Comm.GetTime();
                    peer.publicSend(address, DeliveryMode.Unreliable, new KeepAliveResponseMessage { RequestSendTime = RequestSendTime });
                }
            }
        }

        public class KeepAliveResponseMessage : Message {
            public double RequestSendTime;

            public override void Read(Reader reader) {
                RequestSendTime = reader.ReadDouble();
            }

            public override void Write(Writer writer) {
                writer.WriteDouble(RequestSendTime);
            }

            public override void Handle(Peer peer, IPEndPoint address) {
                PeerData peerData = peer.ConnectedTo != null && Equals(address, peer.ConnectedTo.Address) ? peer.ConnectedTo : peer.FindPeer(address);
                if (peerData != null) {
                    double time = Comm.GetTime();
                    peerData.NextKeepAliveSendTime = time + peer.Settings.KeepAlivePeriod;
                    peerData.Ping = (float)(time - RequestSendTime);
                }
            }
        }

        public class DataMessage : Message {
            public byte[] Bytes;

            public override void Read(Reader reader) {
                Bytes = reader.ReadFixedBytes(reader.Length - reader.Position);
            }

            public override void Write(Writer writer) {
                writer.WriteFixedBytes(Bytes);
            }

            public override void Handle(Peer peer, IPEndPoint address) {
                if (peer.ConnectedTo != null
                    && Equals(address, peer.ConnectedTo.Address)) {
                    peer.DataMessageReceived?.Invoke(new PeerPacket(peer.ConnectedTo, Bytes));
                    return;
                }
                PeerData peerData = peer.FindPeer(address);
                if (peerData != null) {
                    peer.DataMessageReceived?.Invoke(new PeerPacket(peerData, Bytes));
                }
            }
        }

        public volatile bool IsDisposed;

        public Alarm Alarm;

        public double ConnectStartTime;

        public List<PeerData> PeersData = [];

        public Dictionary<IPEndPoint, PeerData> PeersByAddress = new();

        public object Lock => Comm.Lock;

        public Comm Comm { get; set; }

        public PeerSettings Settings { get; } = new();

        public IPEndPoint Address => Comm.Address;

        public PeerData ConnectedTo { get; set; }

        public IPEndPoint ConnectingTo { get; set; }

        public IReadOnlyList<PeerData> Peers {
            get {
                lock (Lock) {
                    CheckNotDisposedAndStarted();
                    return PeersByAddress.Values.ToArray();
                }
            }
        }

        public event Action<Exception> Error;

        public event Action<string> Debug;

        public event Action<Packet> PeerDiscoveryRequest;

        public event Action<Packet> PeerDiscovered;

        public event Action<PeerPacket> ConnectRequest;

        public event Action<PeerPacket> ConnectAccepted;

        public event Action<Packet> ConnectRefused;

        public event Action<IPEndPoint> ConnectTimedOut;

        public event Action Disconnected;

        public event Action<PeerData> PeerConnected;

        public event Action<PeerData> PeerDisconnected;

        public event Action<PeerPacket> DataMessageReceived;

        public Peer(int localPort = 0) : this(new UdpTransmitter(localPort)) { }

        public Peer(ITransmitter transmitter) {
            Comm = new Comm(transmitter);
            Comm.Error += delegate(Exception e) { InvokeError(e); };
            Comm.Received += delegate(Packet packet) {
                try {
                    if (!IsDisposed) {
                        Message.Handle(this, packet);
                    }
                }
                catch (Exception e2) {
                    InvokeError(e2);
                }
            };
        }

        public void Dispose() {
            lock (Lock) {
                if (IsDisposed) {
                    return;
                }
                IsDisposed = true;
            }
            Alarm?.Dispose();
            Comm?.Dispose();
        }

        public void Start() {
            lock (Lock) {
                CheckNotDisposed();
                if (Alarm != null) {
                    throw new InvalidOperationException("Peer is already started.");
                }
                Comm.Start();
                Alarm = new Alarm(AlarmFunction);
                Alarm.Error += delegate(Exception e) { InvokeError(e); };
                Alarm.Set(0.0);
            }
        }

        public PeerData FindPeer(IPEndPoint address) {
            lock (Lock) {
                CheckNotDisposedAndStarted();
                PeerData value;
                return PeersByAddress.TryGetValue(address, out value) ? value : null;
            }
        }

        public void DiscoverLocalPeers(int peerPort, byte[] discoveryQueryData = null) {
            lock (Lock) {
                CheckNotDisposedAndStarted();
                if (Comm.Address.AddressFamily == AddressFamily.InterNetworkV6) {
                    publicSend(
                        new IPEndPoint(UdpTransmitter.IPV6BroadcastAddress, peerPort),
                        DeliveryMode.Raw,
                        new DiscoveryRequestMessage { DiscoveryRequestData = discoveryQueryData ?? [] }
                    );
                }
                else {
                    publicSend(
                        new IPEndPoint(UdpTransmitter.IPV4BroadcastAddress, peerPort),
                        DeliveryMode.Raw,
                        new DiscoveryRequestMessage { DiscoveryRequestData = discoveryQueryData ?? [] }
                    );
                }
            }
        }

        public void DiscoverPeer(IPEndPoint address, byte[] discoveryQueryData = null) {
            lock (Lock) {
                CheckNotDisposedAndStarted();
                publicSend(address, DeliveryMode.Raw, new DiscoveryRequestMessage { DiscoveryRequestData = discoveryQueryData ?? [] });
            }
        }

        public void Connect(IPEndPoint address, byte[] connectRequestData = null) {
            lock (Lock) {
                CheckNotDisposedAndStarted();
                if (ConnectedTo != null) {
                    throw new InvalidOperationException("Peer is already connected.");
                }
                if (ConnectingTo != null) {
                    throw new InvalidOperationException("Peer is already connecting.");
                }
                ConnectingTo = address;
                ConnectStartTime = Comm.GetTime();
                publicSend(address, DeliveryMode.ReliableSequenced, new ConnectRequestMessage { ConnectRequestData = connectRequestData ?? [] });
            }
        }

        public void Disconnect() {
            lock (Lock) {
                CheckNotDisposedAndStarted();
                if (ConnectedTo != null) {
                    publicSend(ConnectedTo.Address, DeliveryMode.ReliableSequenced, new DisconnectRequestMessage());
                    publicDisconnect();
                }
            }
        }

        public void RespondToDiscovery(IPEndPoint address, DeliveryMode deliveryMode, byte[] discoveryResponseData = null) {
            lock (Lock) {
                CheckNotDisposedAndStarted();
                publicSend(address, deliveryMode, new DiscoveryResponseMessage { DiscoveryResponseData = discoveryResponseData ?? [] });
            }
        }

        public void AcceptConnect(PeerData peerData, byte[] connectAcceptedData = null) {
            lock (Lock) {
                CheckNotDisposedAndStarted();
                if (peerData == null
                    || peerData == ConnectedTo
                    || peerData.Address == ConnectingTo
                    || Peers.Contains(peerData)) {
                    throw new ArgumentException("peerData");
                }
                publicSend(
                    peerData.Address,
                    DeliveryMode.ReliableSequenced,
                    new ConnectAcceptedMessage {
                        ConnectAcceptedData = connectAcceptedData ?? [],
                        PeersAddresses = Settings.SendPeerConnectDisconnectNotifications ? PeersByAddress.Keys.ToArray() : []
                    }
                );
                if (Settings.SendPeerConnectDisconnectNotifications
                    && PeersByAddress.Count > 0) {
                    foreach (PeerData value in PeersByAddress.Values) {
                        publicSend(value.Address, DeliveryMode.ReliableSequenced, new PeerConnectedMessage { PeerAddress = peerData.Address });
                    }
                }
                PeersByAddress.Add(peerData.Address, peerData);
                PeerConnected?.Invoke(peerData);
            }
        }

        public void RefuseConnect(PeerData peerData, byte[] refuseConnectData = null) {
            lock (Lock) {
                CheckNotDisposedAndStarted();
                if (peerData == null
                    || peerData == ConnectedTo
                    || peerData.Address == ConnectingTo
                    || Peers.Contains(peerData)) {
                    throw new ArgumentException("peerData");
                }
                publicSend(
                    peerData.Address,
                    DeliveryMode.ReliableSequenced,
                    new ConnectRefusedMessage { ConnectRefusedData = refuseConnectData ?? [] }
                );
            }
        }

        public void SendDataMessage(PeerData peerData, DeliveryMode deliveryMode, byte[] bytes) {
            lock (Lock) {
                CheckNotDisposedAndStarted();
                if (peerData == null
                    || (peerData != ConnectedTo && !Peers.Contains(peerData))) {
                    throw new ArgumentException("peerData");
                }
                publicSend(peerData.Address, deliveryMode, new DataMessage { Bytes = bytes });
            }
        }

        public void SendDataMessages(PeerData peerData, DeliveryMode deliveryMode, IEnumerable<byte[]> bytes) {
            lock (Lock) {
                CheckNotDisposedAndStarted();
                if (peerData == null
                    || (peerData != ConnectedTo && !Peers.Contains(peerData))) {
                    throw new ArgumentException("peerData");
                }
                IEnumerable<DataMessage> messages = bytes.Select(b => new DataMessage { Bytes = b });
                publicSend(peerData.Address, deliveryMode, messages);
            }
        }

        public void DisconnectPeer(PeerData peerData) {
            lock (Lock) {
                CheckNotDisposedAndStarted();
                if (!PeersByAddress.ContainsValue(peerData)) {
                    return;
                }
                publicSend(peerData.Address, DeliveryMode.ReliableSequenced, new DisconnectedMessage());
                PeersByAddress.Remove(peerData.Address);
                if (Settings.SendPeerConnectDisconnectNotifications) {
                    foreach (PeerData value in PeersByAddress.Values) {
                        publicSend(value.Address, DeliveryMode.ReliableSequenced, new PeerDisconnectedMessage { PeerAddress = peerData.Address });
                    }
                }
                PeerDisconnected?.Invoke(peerData);
            }
        }

        public void DisconnectAllPeers() {
            lock (Lock) {
                CheckNotDisposedAndStarted();
                PeerData[] array = PeersByAddress.Values.ToArray();
                foreach (PeerData peerData in array) {
                    DisconnectPeer(peerData);
                }
            }
        }

        public void AlarmFunction() {
            lock (Lock) {
                if (!IsDisposed) {
                    ProcessPeers();
                    float num = 0.25f;
                    float num2 = Math.Min(
                        Settings.KeepAlivePeriod,
                        Math.Min(Settings.KeepAliveResendPeriod, Math.Min(Settings.ConnectTimeOut, Settings.ConnectionLostPeriod))
                    );
                    Alarm.Set(Math.Max(num2 * num, 0.01f));
                }
            }
        }

        public void ProcessPeers() {
            PeersData.Clear();
            if (ConnectedTo != null) {
                PeersData.Add(ConnectedTo);
            }
            else {
                PeersData.AddRange(PeersByAddress.Values);
            }
            double time = Comm.GetTime();
            foreach (PeerData peersDatum in PeersData) {
                double num = time - peersDatum.LastKeepAliveReceiveTime;
                if (num >= Settings.ConnectionLostPeriod) {
                    if (peersDatum == ConnectedTo) {
                        InvokeError(new KeepAliveTimeoutException($"Server {peersDatum.Address} did not respond for {num:0.0}s, disconnecting."));
                        Disconnect();
                    }
                    else {
                        InvokeError(new KeepAliveTimeoutException($"Peer {peersDatum.Address} did not respond for {num:0.0}s, disconnecting."));
                        DisconnectPeer(peersDatum);
                    }
                }
                else if (time >= peersDatum.NextKeepAliveSendTime) {
                    publicSend(peersDatum.Address, DeliveryMode.Unreliable, new KeepAliveRequestMessage { RequestSendTime = time });
                    peersDatum.NextKeepAliveSendTime = time + Settings.KeepAliveResendPeriod;
                }
            }
            if (ConnectingTo != null
                && time - ConnectStartTime >= Settings.ConnectTimeOut) {
                IPEndPoint connectingTo = ConnectingTo;
                ConnectingTo = null;
                try {
                    ConnectTimedOut?.Invoke(connectingTo);
                }
                catch (Exception e) {
                    InvokeError(e);
                }
            }
        }

        public void publicDisconnect() {
            ConnectedTo = null;
            ConnectingTo = null;
            PeersByAddress.Clear();
            Disconnected?.Invoke();
        }

        public void publicSend(IPEndPoint address, DeliveryMode deliveryMode, Message message) {
            byte[] bytes = Message.Write(message);
            Comm.Send(address, deliveryMode, bytes);
        }

        public void publicSend(IPEndPoint address, DeliveryMode deliveryMode, IEnumerable<Message> messages) {
            List<byte[]> list = [];
            foreach (Message message in messages) {
                list.Add(Message.Write(message));
            }
            Comm.Send(address, deliveryMode, list);
        }

        public void CheckNotDisposed() {
            if (IsDisposed) {
                throw new ObjectDisposedException("Peer");
            }
        }

        public void CheckNotDisposedAndStarted() {
            CheckNotDisposed();
            if (Alarm == null) {
                throw new InvalidOperationException("Peer is not started.");
            }
        }

        public void InvokeError(Exception e) {
            Error?.Invoke(e);
        }

        [Conditional("DEBUG")]
        public void InvokeDebug(string format, params object[] args) {
            if (Debug != null) {
                Debug?.Invoke(string.Format(format, args));
            }
        }
    }
}